/*=========================================================================
| KCalDAV
|--------------------------------------------------------------------------
| (c) 2009  Kumaran Santhanam
|
| This project is released under the GNU General Public License.
| Please see the file COPYING for more details.
|--------------------------------------------------------------------------
| Main interface to the KResource system.
 ========================================================================*/

/*=========================================================================
| INCLUDES
 ========================================================================*/

#include "resource.h"
#include "reader.h"
#include "writer.h"

#include <kconfiggroup.h>

#include <string.h>

#include <QtCore/QUrl>
#include <QtGui/QMessageBox>

#include <kcal/calendarlocal.h>
#include <kcal/icalformat.h>

#include <QtCore/QDate>
#include <QtCore/QDateTime>
#include <QtCore/QMutex>
#include <QtCore/QMutexLocker>

#ifdef KCALDAV_DEBUG
    #include <QtCore/QFile>
    #include <QtCore/QTextStream>
#endif

/*=========================================================================
| NAMESPACE
 ========================================================================*/

using namespace KCal;

/*=========================================================================
| CONSTANTS
 ========================================================================*/

const unsigned long ResourceCalDav::TERMINATION_WAITING_TIME = 3 * 1000; // 3 seconds

const int ResourceCalDav::CACHE_DAYS_PAST   = 180;
const int ResourceCalDav::CACHE_DAYS_FUTURE = 365;

const int ResourceCalDav::DEFAULT_RELOAD_INTERVAL   = 10;
const int ResourceCalDav::DEFAULT_SAVE_INTERVAL     = 10;
const int ResourceCalDav::DEFAULT_RELOAD_POLICY     = ResourceCached::ReloadInterval;
const int ResourceCalDav::DEFAULT_SAVE_POLICY       = ResourceCached::SaveDelayed;

/*=========================================================================
| UTILITY
 ========================================================================*/

#define log(s)  kDebug() << identifier() << ": " << (s);

/*=========================================================================
| CONSTRUCTOR / DESTRUCTOR
 ========================================================================*/

ResourceCalDav::ResourceCalDav() :
    ResourceCached()
    , mLock(true)
    , mPrefs(NULL)
    , mLoader(NULL)
    , mWriter(NULL)
    , mLoadingQueueReady(true)
    , mWritingQueueReady(true)
{
    log("ResourceCalDav()");
    init();
}

ResourceCalDav::ResourceCalDav( const KConfigGroup &group ) :
    ResourceCached(group)
    , mLock(true)
    , mPrefs(NULL)
    , mLoader(NULL)
    , mWriter(NULL)
    , mLoadingQueueReady(true)
    , mWritingQueueReady(true)
{
    log("ResourceCalDav(group)");
    init();
    readConfig(group);
}

ResourceCalDav::~ResourceCalDav() {
    log("jobs termination");

    // TODO: do we need termination here?

    if (mLoader) {
        mLoader->terminate();
    }
    if (mWriter) {
        mWriter->terminate();
    }

    log("waiting for jobs terminated");

    if (mLoader) {
        mLoader->wait(TERMINATION_WAITING_TIME);
    }
    if (mWriter) {
        mWriter->wait(TERMINATION_WAITING_TIME);
    }

    log("deleting jobs");

    delete mLoader;
    delete mWriter;

    log("deleting preferences");

    delete mPrefs;
}

/*=========================================================================
| GENERAL METHODS
 ========================================================================*/

bool ResourceCalDav::doLoad( bool syncCache ) {
    log(QString("doLoad(%1)").arg(syncCache));

    log("clearing changes");
    clearChanges();

    log("loading from cache");
    {
        ChangeNotificationHandler change_handler(this);
        //clearCache();
        loadFromCache();
        change_handler.setEmitResourceChanged(true);
    }

    log("starting download job");
    startLoading(mPrefs->getFullUrl());

    return true;
}

bool ResourceCalDav::doSave( bool syncCache ) {
    log(QString("doSave(%1)").arg(syncCache));

    if (!hasChanges()) {
        log("no changes");
        return true;
    }

    log("saving cache");
    saveToCache();

    if (syncCache) {
        log("start writing job");
        startWriting(mPrefs->getFullUrl());

        log("clearing changes");
        // FIXME: Calling clearChanges() here is not the ideal way since the
        // upload might fail, but there is no other place to call it...
        clearChanges();
    }

    return true;
}

bool ResourceCalDav::doSave( bool syncCache, Incidence *incidence ) {
    // To suppress warning about doSave(bool) method hides ResourceCached::doSave(bool, Incidence)
    return ResourceCached::doSave(syncCache, incidence);
}

KABC::Lock* ResourceCalDav::lock() {
    log("lock()");
    return &mLock;
}

void ResourceCalDav::readConfig( const KConfigGroup &group ) {
    log("readConfig");
    mPrefs->readConfig();
    ResourceCached::readConfig(group);
}

void ResourceCalDav::writeConfig( KConfigGroup &group ) {
    log("writeConfig()");
    ResourceCalendar::writeConfig(group);
    mPrefs->writeConfig();
    ResourceCached::writeConfig(group);
}

CalDavPrefs* ResourceCalDav::createPrefs() const {
    log("createPrefs()");
    CalDavPrefs* p = new CalDavPrefs(identifier());
    return p;
}

void ResourceCalDav::init() {
    // default settings
    setReloadInterval(DEFAULT_RELOAD_INTERVAL);
    setReloadPolicy(DEFAULT_RELOAD_POLICY);
    setSaveInterval(DEFAULT_SAVE_INTERVAL);
    setSavePolicy(DEFAULT_SAVE_POLICY);

    // creating preferences
    mPrefs = createPrefs();

    // creating jobs
    mLoader = new CalDavReader;
    QObject::connect(mLoader, SIGNAL(finished()), this, SLOT(loadFinished()));
    mWriter = new CalDavWriter;
    QObject::connect(mWriter, SIGNAL(finished()), this, SLOT(writingFinished()));

    setType("ResourceCalDav");
}

void ResourceCalDav::setIncidencesReadOnly(Incidence::List& inc, bool readOnly) {
    Incidence::List::Iterator it;
    for ( it = inc.begin(); it != inc.end(); ++it ) {
        (*it)->setReadOnly( readOnly );
    }
}

void ResourceCalDav::ensureReadOnlyFlagHonored() {
    Incidence::List inc( rawIncidences() );
    setIncidencesReadOnly(inc, readOnly());
}

void ResourceCalDav::setReadOnly(bool v) {
    KRES::Resource::setReadOnly(v);
    log("ensuring read only flag honored");
    ensureReadOnlyFlagHonored();
}

/*=========================================================================
| READING METHODS
 ========================================================================*/

void ResourceCalDav::loadingQueuePush(const LoadingTask& task) {
    log("");
    mLoadingQueue.enqueue(task);
    loadingQueuePop();
}

void ResourceCalDav::loadingQueuePop() {
    log("");

    if (!mLoadingQueueReady || mLoadingQueue.isEmpty()) {
        log("loading queue is not ready or empty");
        return;
    }

    if (!mLoader) {
        log("loader == NULL");
        return;
    }

    log("loading task is found, initiating job");

    // Loading queue and mLoadingQueueReady flag are not shared resources, i.e. only one thread has an access to them.
    // That's why no mutexes are required.
    LoadingTask t = mLoadingQueue.head();

    mLoader->setUrl(t.url);
    mLoader->setEncoding(mPrefs->encoding());

    QDateTime dt(QDate::currentDate());
    mLoader->setRange(dt.addDays(-CACHE_DAYS_PAST),
                      dt.addDays(+CACHE_DAYS_FUTURE));
    //mLoader->setGetAll();

    mLoadingQueueReady = false;

    log("starting actual download job");
    mLoader->start(QThread::LowestPriority);

    // if all ok, removing the task from the queue
    mLoadingQueue.dequeue();
}

void ResourceCalDav::startLoading(const QString& url) {
    log("");
    LoadingTask t;
    t.url = url;
    loadingQueuePush(t);
}

void ResourceCalDav::loadFinished() {
    CalDavReader* loader = mLoader;

    log("load finished");

    if (!loader) {
        log("loader is NULL");
        return;
    }

    if (loader->error()) {
        log("load error: " + loader->errorString() );
        loadError(loader->errorString());
    } else {
        log("successful load");
        QString data = loader->data();

        if (!data.isNull() && !data.isEmpty()) {
            // TODO: I don't know why, but some schedules on http://caldav-test.ioda.net/ (I used it for testing)
            // have some lines separated by single \r rather than \n or \r\n.
            // ICalFormat fails to parse that.
            data.replace("\r\n", "\n"); // to avoid \r\n becomes \n\n after the next line
            data.replace('\r', '\n');

            // There is a bug in the library which causes & to be read as &amp;
            data.replace("&amp;", "&");

            log("trying to parse...");
            if (parseData(data)) {
                log("... parsing is ok");
                setReloaded(true);
                emit resourceLoaded(this);
            }
        } else {
            // empty data, clearing cache

            clearChanges();
            clearCache();
            emit resourceChanged(this);

            saveToCache();
            setReloaded(true);
            emit resourceLoaded(this);
        }
    }

    log("loading queue is ready, check for tasks");
    // Loading queue and mLoadingQueueReady flag are not shared resources, i.e. only one thread has an access to them.
    // That's why no mutexes are required.
    mLoadingQueueReady = true;
    loadingQueuePop();
}

bool ResourceCalDav::checkData(const QString& data) {
    log("checking the data");

    ICalFormat ical;
    bool ret = true;
    CalendarLocal tmp_cal(calendar()->timeZoneId());
    if (!ical.fromString(&tmp_cal, data)) {
        log("invalid ical string");
        ret = false;
    }

    return ret;
}

bool ResourceCalDav::parseData(const QString& data) {
    log("parseData()");

    bool ret = true;

    // check if the data is OK
    // May be it's not efficient (parsing is done twice), but it should be safe
    if (!checkData(data)) {
        loadError(i18n("Parsing calendar data failed."));
        return false;
    }

    ChangeNotificationHandler change_handler(this);

    log("clearing cache");
    clearChanges();
    clearCache();
    change_handler.setEmitResourceChanged(true);

    log("actually parsing the data");

    calendar()->close();
    ICalFormat ical;
    if ( !ical.fromString( calendar(), data ) ) {
        // this should never happen, but...
        ret = false;
    }

    // debug code here -------------------------------------------------------
#ifdef KCALDAV_DEBUG
    const QString fout_path = "/tmp/kcaldav_download_" + identifier() + ".tmp";

    QFile fout(fout_path);
    if (fout.open(QFile::WriteOnly | QFile::Append)) {
        QTextStream sout(&fout);
        sout << "---------- " << resourceName() << ": --------------------------------\n";
        sout << data << "\n";
        fout.close();
    } else {
        //loadError(i18n("can't open file"));
    }
#endif // KCALDAV_DEBUG
    // end of debug code ----------------------------------------------------

    if (ret) {
        log("parsing is ok");
        if ( !noReadOnlyOnLoad() && readOnly() ) {
            log("ensuring read only flag honored");
            ensureReadOnlyFlagHonored();
        }
        log("saving to cache");
        saveToCache();
    }

    return ret;
}

/*=========================================================================
| WRITING METHODS
 ========================================================================*/

QString ResourceCalDav::getICalString(const Incidence::List& inc) const {
    if (inc.isEmpty()) {
        return "";
    }

    CalendarLocal loc(calendar()->timeZoneId());
    QString data = "";
    ICalFormat ical;

    for(Incidence::List::ConstIterator it = inc.constBegin(); it != inc.constEnd(); ++it) {
        Incidence *inc = (*it)->clone();
        loc.addIncidence(inc);
        data += ical.toString(&loc);
        loc.deleteIncidence(inc);
        delete inc;
    }

    return data;
}

void ResourceCalDav::writingQueuePush(const WritingTask& task) {
    log("");
    mWritingQueue.enqueue(task);
    writingQueuePop();
}

void ResourceCalDav::writingQueuePop() {
    log("");

    if (!mWritingQueueReady || mWritingQueue.isEmpty()) {
        log("writing queue is not ready or empty");
        return;
    }

    if (!mWriter) {
        log("writer == NULL");
        return;
    }

    log("writing task is found, initiating job");

    // Writing queue and mWritingQueueReady flag are not shared resources, i.e. only one thread has an access to them.
    // That's why no mutexes are required.
    WritingTask t = mWritingQueue.head();

    log("writingQueuePop: url = " + t.url);

    mWriter->setUrl(t.url);
    mWriter->setEncoding(mPrefs->encoding());

#ifdef KCALDAV_DEBUG
    const QString fout_path = "/tmp/kcaldav_upload_" + identifier() + ".tmp";

    QFile fout(fout_path);
    if (fout.open(QFile::WriteOnly | QFile::Append)) {
        QTextStream sout(&fout);
        sout << "---------- " << resourceName() << ": --------------------------------\n";
        sout << "================== Added:\n" << t.added << "\n";
        sout << "================== Changed:\n" << t.changed << "\n";
        sout << "================== Deleted:\n" << t.deleted << "\n";
        fout.close();
    }
#endif // debug

    mWriter->setAddedObjects(t.added);
    mWriter->setChangedObjects(t.changed);
    mWriter->setDeletedObjects(t.deleted);

    mWritingQueueReady = false;

    log("starting actual write job");
    mWriter->start(QThread::LowestPriority);

    // if all ok, remove the task from the queue
    mWritingQueue.dequeue();
}

void ResourceCalDav::startWriting(const QString& url) {
    log("startWriting: url = " + url);

    WritingTask t;

    t.url = url;
    t.added = getICalString(addedIncidences());
    t.changed = getICalString(changedIncidences());
    t.deleted = getICalString(deletedIncidences());

    writingQueuePush(t);
}

void ResourceCalDav::writingFinished() {
    log("writing finished");

    if (!mWriter) {
        log("mWriter is NULL");
        return;
    }

    if (mWriter->error()) {
        log("error: " + mWriter->errorString());
        saveError(mWriter->errorString());
    } else {
        log("success");
        emit resourceSaved(this);
    }

    log("writing queue is ready, check for tasks");
    // Writing queue and mWritingQueueReady flag are not shared resources, i.e. only one thread has an access to them.
    // That's why no mutexes are required.
    mWritingQueueReady = true;
    writingQueuePop();
}

// EOF ========================================================================
